﻿
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Windows;
using System.Windows.Media;


namespace Boilen.Primitives {

    /// <summary>
    /// Returns the string representation of types, saving the used namespaces.
    /// </summary>
    public sealed class TypeRepository {

        /// <summary>
        /// Gets the collection of type namespaces added automatically through <see cref="GetTypeName"/> or manually through <see cref="AddNamespace"/>.
        /// </summary>
        public IEnumerable<string> UsedNamespaces { get { return this.namespaces_.Where( n => n.Length > 0 ); } }

        /// <summary>
        /// Adds a global name alias for the specified type.
        /// </summary>
        /// <param name="type">The type to alias.</param>
        /// <param name="alias">The alias name.</param>
        /// <returns>An object that can be used to remove the global alias.</returns>
        public static IDisposable AddGlobalAlias( Type type, string alias ) {
            Ensure.NotNull( type );
            Ensure.NotNullOrEmpty( alias );

            AliasedTypes[type] = alias;
            return new RemoveGlobalAlias( type );
        }

        /// <summary>
        /// Adds a namespace to the collection of used namespaces.
        /// </summary>
        /// <param name="nmspace">The namespace to add.</param>
        public void AddNamespace( string nmspace ) {
            Ensure.NotNull( nmspace );

            if( !this.namespaces_.Contains( nmspace ) )
                this.namespaces_.Add( nmspace );
        }

        /// <summary>
        /// Adds the namespace of a type to the collection of used namespaces.
        /// </summary>
        /// <param name="type">The type to add a namespace for.</param>
        public void AddNamespace( Type type ) {
            Ensure.NotNull( type );

            string nmspace = GetTypeNamespaceCore( type );
            this.AddNamespace( nmspace );
        }

        /// <summary>
        /// Gets the namespace of the specified type.
        /// </summary>
        /// <param name="type">The type to use.</param>
        /// <returns>The namespace of <paramref name="type"/>.</returns>
        public string GetTypeNamespace( Type type ) {
            Ensure.NotNull( type );

            return GetTypeNamespaceCore( type );
        }

        /// <summary>
        /// Gets the name of the specified type that can be used in a generated code file.
        /// </summary>
        /// <param name="type">The type to name.</param>
        /// <returns>The name of <paramref name="type"/>.</returns>
        public string GetTypeName( Type type ) {
            Ensure.NotNull( type );

            if( !this.typeNames_.ContainsKey( type ) )
                this.typeNames_[type] = this.GetTypeNameCore( type );
            return this.typeNames_[type];
        }

        /// <summary>
        /// Gets the simple name of the type, removing any generic parameters.
        /// </summary>
        /// <param name="type">The type to name.</param>
        /// <returns>The simple name of <paramref name="type"/>, without any generic parameters.</returns>
        public string GetSimpleTypeName( Type type ) {
            Ensure.NotNull( type );

            if( type.IsArray )
                return GetSimpleTypeName( type.GetElementType( ) );

            string typeName = CommonTypes.ContainsKey( type ) ? CommonTypes[type] : type.Name;
            int index = typeName.IndexOf( '`' );
            if( index > 0 )
                typeName = typeName.Substring( 0, index );

            return typeName;
        }


        /// <summary>
        /// Gets the string representation of the specified value.
        /// </summary>
        /// <typeparam name="T">The type of the value.</typeparam>
        /// <param name="value">The value to retrieve a string representation for.</param>
        /// <param name="silverlight">Whether the value needs to be Silverlight-compatible.</param>
        /// <returns>The string representation of <paramref name="value"/>.</returns>
        public string GetValueString<T>( T value, bool silverlight ) {
            if( value == null )
                return "null";

            string stringValue = value.ToString( );

            if( value is string )
                return EscapeString( stringValue );

            if( typeof( T ).IsEnum ) {
                var flags = stringValue.Split( ',' );
                string valueTypeName = this.GetTypeName( typeof( T ) );
                return Util.Join( flags, " | ", ( i, flag ) => valueTypeName + "." + flag.Trim( ) );
            }

            var boolean = value as bool?;
            if( boolean != null )
                return boolean.Value ? "true" : "false";

            var real = value as double?;
            if( real != null ) {
                if( double.IsNaN( real.Value ) )
                    return "double.NaN";
                if( double.IsPositiveInfinity( real.Value ) )
                    return "double.PositiveInfinity";
                if( double.IsNegativeInfinity( real.Value ) )
                    return "double.NegativeInfinity";
            }

            var size = value as Size?;
            if( size != null ) {
                if( size.Value.IsEmpty )
                    return "Size.Empty";

                string widthValueString = GetValueString( size.Value.Width, silverlight );
                string heightValueString = GetValueString( size.Value.Height, silverlight );
                return string.Format( "new {0}({1}, {2})", typeof( Size ).Name, widthValueString, heightValueString );
            }

            var point = value as Point?;
            if( point != null ) {
                string widthValueString = GetValueString( point.Value.X, silverlight );
                string heightValueString = GetValueString( point.Value.Y, silverlight );
                return string.Format( "new {0}({1}, {2})", typeof( Point ).Name, widthValueString, heightValueString );
            }

            var thickness = value as Thickness?;
            if( thickness != null ) {
                string topValueString = GetValueString( thickness.Value.Top, silverlight );
                string leftValueString = GetValueString( thickness.Value.Left, silverlight );
                string rightValueString = GetValueString( thickness.Value.Right, silverlight );
                string bottomValueString = GetValueString( thickness.Value.Bottom, silverlight );
                bool same = new HashSet<string> { topValueString, leftValueString, rightValueString, bottomValueString }.Count == 1;
                string format = "new {0}({1}" + (same ? "" : ", {2}, {3}, {4}") + ")";
                return string.Format( format, typeof( Thickness ).Name, leftValueString, topValueString, rightValueString, bottomValueString );
            }

            var brush = value as SolidColorBrush;
            if( brush != null ) {
                Color color = brush.Color;
                string colorName = GetColorName( color );

                string format;
                if( colorName == null )
                    format = "new SolidColorBrush(Color.FromArgb(0x{1:x}, 0x{2:x}, 0x{3:x}, 0x{4:x}))";
                else if( silverlight )
                    format = "new SolidColorBrush(Colors.{0})";
                else
                    format = "Brushes.{0}";

                return string.Format( format, colorName, color.A, color.R, color.G, color.B );
            }

            var type = value as Type;
            if( type != null )
                return string.Format( "typeof({0})", this.GetTypeName( type ) );

            return stringValue;
        }


        /// <summary>
        /// Escapes the specified string value.
        /// </summary>
        /// <param name="value">The value to escape.</param>
        /// <returns>The escaped string value.</returns>
        public static string EscapeString( string value ) {
            return value.Length == 0
                 ? "string.Empty"
                 : '"' + value + '"';
        }


        #region Private

        private static readonly Dictionary<Color, string> Colors = typeof( Colors )
            .GetProperties( BindingFlags.Public | BindingFlags.Static )
            .Distinct( new PropertyComparer( ) )
            .ToDictionary( p => (Color)p.GetValue( null, null ), p => p.Name );

        private static string GetColorName( Color color ) {
            return Colors.ContainsKey( color ) ? Colors[color] : null;
        }

        private sealed class PropertyComparer : IEqualityComparer<PropertyInfo> {
            public bool Equals( PropertyInfo x, PropertyInfo y ) {
                object xValue = x.GetValue( null, null );
                object yValue = y.GetValue( null, null );
                return object.Equals( xValue, yValue );
            }

            public int GetHashCode( PropertyInfo obj ) {
                object value = obj.GetValue( null, null );
                return value.GetHashCode( );
            }
        }

        private sealed class RemoveGlobalAlias : IDisposable {
            private readonly Type type_;

            public RemoveGlobalAlias( Type type ) {
                this.type_ = type;
            }

            public void Dispose( ) {
                AliasedTypes.Remove( this.type_ );
            }
        }



        private readonly List<string> namespaces_ = new List<string>( );

        private readonly Dictionary<Type, string> typeNames_ = new Dictionary<Type, string>( );

        private static readonly Dictionary<Type, string> CommonTypes = new Dictionary<Type, string>{
            { typeof( void ),   "void" },
            { typeof( object ), "object" },
            { typeof( bool ),   "bool" },
            { typeof( char ),   "char" },
            { typeof( int ),    "int" },
            { typeof( uint ),   "uint" },
            { typeof( long ),   "long" },
            { typeof( double ), "double" },
            { typeof( string ), "string" },
        };

        private static readonly Dictionary<Type, string> AliasedTypes = new Dictionary<Type, string>( );


        private static string GetTypeNamespaceCore( Type type ) {
            if( type.IsArray )
                return GetTypeNamespaceCore( type.GetElementType( ) );

            var templateType = Util.GetAttribute<TemplateTypeAttribute>( type );
            if( templateType != null )
                return templateType.Namespace ?? "";

            var aliasType = Util.GetAttribute<AliasTypeAttribute>( type );
            if( aliasType != null )
                return GetAliasTypeNamespaceCore( type.Name, aliasType.Alias );

            if( AliasedTypes.ContainsKey( type ) )
                return GetAliasTypeNamespaceCore( AliasedTypes[type], type );

            return type.Namespace;
        }

        private static string GetAliasTypeNamespaceCore( string alias, Type aliasedType ) {
            return alias + " = " + aliasedType.FullName;
        }

        private string GetTypeNameCore( Type type ) {
            string typeNamespace = GetTypeNamespaceCore( type );
            if( !string.IsNullOrEmpty( typeNamespace ) )
                this.AddNamespace( typeNamespace );

            if( type.IsGenericType )
                return type.GetGenericArguments( ).Aggregate(
                    new StringBuilder( type.Name.Substring( 0, type.Name.IndexOf( '`' ) ) + "<" ),
                    ( sb, t ) => sb.Append( this.GetTypeName( t ) ).Append( ", " ),
                    ( sb ) => { sb[sb.Length - 2] = '>'; return sb.ToString( 0, sb.Length - 1 ); }
                );

            if( CommonTypes.ContainsKey( type ) )
                return CommonTypes[type];

            if( AliasedTypes.ContainsKey( type ) )
                return AliasedTypes[type];

            if( type.IsArray ) {
                int rank = type.GetArrayRank( );
                Type elementType = type.GetElementType( );
                return GetTypeNameCore( elementType ) + "[" + new string( ',', rank - 1 ) + "]";
            }

            return type.Name;
        }

        #endregion

    }

}
